package cn.learn.validate.impl;

import cn.learn.properties.MySecurityConstants;
import cn.learn.validate.*;
import cn.learn.validate.sms.SmsCode;
import io.micrometer.core.instrument.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;

/**
 * 验证码处理器
 * 不同类型的验证码自己继承这个做自己需要的业务
 * 如果所有逻辑都变了，则自己实现 {@link ValidateCodeProcessor}
 *
 * @author huangyezhan
 * @date 2020年2月20日23:53:50
 */
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {
    /**
     * 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。
     */
    @Autowired
    private Map<String, ValidateCodeGenerator> validateCodeGenerators;


    /**
     * 创建验证码分三步骤：
     * 1、生成验证码需要的消息
     * 2、保存验证码（做校验用）
     * 3、发送验证码给用户
     *
     * @param request  请求
     * @param response 响应
     */
    @Override
    public void createCode(HttpServletRequest request, HttpServletResponse response) {
        C validateCode = this.generate(request);
        this.save(request, validateCode);
        this.send(response, validateCode);
    }

    /**
     * 生成校验码
     *
     * @param request 请求
     * @return 返回继承于 {@link cn.learn.validate.ValidateCode} 的类
     */
    private C generate(HttpServletRequest request) {
        ValidateCodeGenerator validateCodeGeneratorHolder = this.getValidateCodeGeneratorHolder(request);
        return (C) validateCodeGeneratorHolder.generate(request);
    }

    /**
     * 保存校验码到session里面，保存的时候要先获取session里面的数据查看之前是否还有校验失败的
     * 如果有，则更新数据；反之直接存进去
     *
     * @param request      请求
     * @param validateCode 验证码的对象
     */
    protected void save(HttpServletRequest request, C validateCode) {
        request.getSession().setAttribute(getSessionKey(request), validateCode);
    }

    /**
     * 发送校验码，由子类实现
     *
     * @param response     响应
     * @param validateCode 验证码的相关消息
     */
    protected abstract void send(HttpServletResponse response, C validateCode);

    /**
     * 获取验证码生成器持有者的逻辑，可以参考{@link ValidateCodeProcessorHolder}的做法
     *
     * @param request 请求
     * @return
     */
    protected ValidateCodeGenerator getValidateCodeGeneratorHolder(HttpServletRequest request) {
        //获取验证码类型
        String                validateCodeType      = this.getValidateCodeType().toString().toLowerCase();
        String                generatorName         = validateCodeType + ValidateCodeGenerator.class.getSimpleName();
        ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(generatorName);
        if (validateCodeGenerator == null) {
            throw new ValidateCodeException("验证码生成器" + generatorName + "不存在");
        }
        return validateCodeGenerator;
    }

    /**
     * 构建验证码放入session时的key
     *
     * @param request 请求
     * @return 返回一个sessionKey
     */
    protected String getSessionKey(HttpServletRequest request) {
        return SESSION_KEY_PREFIX + this.getValidateCodeType().toString().toUpperCase();
    }

    /**
     * 三种做法：这里、{@link ValidateCodeFilter}、{@link ValidateCodeProcessorHolder}的获取访问类型
     * 根据请求的url获取校验码的类型，在访问controller的时候就已经获取过了type了
     * 这里是根据request获取出类型，如果有问题在{@link ValidateCodeProcessorHolder}就抛异常了
     */
    private ValidateCodeType getValidateCodeType() {
        String simpleName = this.getClass().getSimpleName();
        String type       = simpleName.substring(0, simpleName.indexOf(ValidateCodeProcessor.class.getSimpleName()));
        return ValidateCodeType.valueOf(type.toUpperCase());
    }

    /**
     * 校验验证码：
     * 1、校验验证码之前，应该还要校验当前的账户是否已经校验次数过多暂时被锁？？？
     *
     * @param request 请求
     */
    @Override
    public void validate(HttpServletRequest request) {
        ValidateCodeType processorType = getValidateCodeType();
        String           sessionKey    = getSessionKey(request);

        HttpSession session       = request.getSession();
        C           codeInSession = (C) session.getAttribute(sessionKey);

        //校验手机号码
        if (processorType == ValidateCodeType.SMS) {
            this.validateMobile(request, codeInSession);
        }

        String codeInRequest = request.getParameter(processorType.getParamNameOnValidate());
        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException(processorType + "验证码的值不能为空");
        }

        if (codeInSession == null) {
            throw new ValidateCodeException(processorType + "验证码不存在");
        }
        if (codeInSession.isExpried()) {
            session.removeAttribute(sessionKey);
            throw new ValidateCodeException(processorType + "验证码已过期");
        }
        if (!codeInSession.getCode().equalsIgnoreCase(codeInRequest)) {
            throw new ValidateCodeException(processorType + "验证码不匹配");
        }

        session.removeAttribute(sessionKey);
    }

    /**
     * 校验登录手机号码与发送短信验证码的手机号码是否一致
     *
     * @param request       请求,获取请求的手机号码
     * @param codeInSession 存在session里面的对象
     */
    protected void validateMobile(HttpServletRequest request, C codeInSession) {
        String  parameter   = request.getParameter(MySecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE);
        SmsCode smsCode     = (SmsCode) codeInSession;
        String  phoneNumber = smsCode.getPhoneNumber();

        if (StringUtils.isBlank(parameter)) {
            throw new ValidateCodeException(ValidateCodeType.SMS + "手机号不能为空");
        }
        if (!parameter.equals(phoneNumber)) {
            throw new ValidateCodeException(ValidateCodeType.SMS + "手机号不匹配");
        }
    }

}
